package Domain;

import Persistence.PersistenceController;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author kenneth
 */
public class ClientConnection implements Runnable {

    private static final int BUFFERSIZE = 1024;
    private Socket link;
    private ObservableManager observableManager;
    private String email;
    private boolean run;
    private DataInputStream socketIn;
    private DataOutputStream socketOut;
    //hopelijk
    File dir;

    /*
     ** GENERAL
     */
    public ClientConnection(Socket link, ObservableManager observableManager) {
        this.link = link;
        this.observableManager = observableManager;
        initConnection();
    }

    @Override
    public void run() {
        run = true;

        while (run) {
            String state = "";
            try {
                System.out.println(state);
                state = socketIn.readUTF();
                System.out.println(state);
            } catch (IOException ex) {
                ex.printStackTrace();
                observableManager.updateMessage("Connection with client failed");
                run = false;
            }
            if (state.equals("login")) {
                login();
            } else if (state.equals("register")) {
                register();
            } else if (state.equals("resetPassword")) {
                resetPassword();
            } else if (state.equals("acceptFile")) {
                acceptFile();
            } else if (state.equals("deleteFile")) {
                deleteFile();
            } //else if(state.equals("checkFile"))
            //checkFile();
            else if (state.equals("synchronize")) {
                synchronizeFiles();
            } else if (state.equals("closeConnection")) {
                closeConnection();
            }

            try {
                System.out.println("sleep");
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                run = false;
                Logger.getLogger(ClientConnection.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /*
     ** INITIALIZATION
     */
    private void initConnection() {
        try {
            socketIn = new DataInputStream(link.getInputStream());
            socketOut = new DataOutputStream(link.getOutputStream());
        } catch (IOException e) {
            observableManager.updateMessage("Opening streams failed");
        }
    }

    private void closeConnection() {
        try {
            link.close();
            observableManager.updateMessage("Closing " + email + "'s connection");
        } catch (IOException ex) {
            observableManager.updateMessage(email + "'s connection was already closed");
        }
    }

    /*
     ** ACCOUNT
     */
    private void login() {
        String password = "";
        try {
            email = socketIn.readUTF();
            password = socketIn.readUTF();
            email = "kenneth.de.clercq@telenet.be"; // TEMP
        } catch (IOException ex) {
            ex.printStackTrace();
            observableManager.updateMessage("Connection with client failed");
        }
        //boolean loginSuccess = PersistenceController.getInstance().authenticateUser(email, password);
        boolean loginSuccess = true; // TEMP
        if (loginSuccess) {
            observableManager.updateMessage("User " + email + " has authenticated");
            checkAndCreateFolder();
        } else {
            observableManager.updateMessage("User " + email + " has failed to authenticate");
        }
        sendResult(loginSuccess);
    }

    private void register() {
        String password = "";
        try {
            email = socketIn.readUTF();
            password = socketIn.readUTF();
        } catch (IOException ex) {
            observableManager.updateMessage("Connection with client failed");
        }
        boolean registerSuccess = PersistenceController.getInstance().registerUser(email, password);
        if (registerSuccess) {
            observableManager.updateMessage("User " + email + " has registered an account");
            checkAndCreateFolder();
        } else {
            observableManager.updateMessage("User " + email + " has failed to register an account");
        }
        sendResult(registerSuccess);
    }

    private void resetPassword() {
        try {
            email = socketIn.readUTF();
        } catch (IOException ex) {
            observableManager.updateMessage("Connection with client failed");
        }
        boolean resetPasswordSuccess = PersistenceController.getInstance().resetPasswordFromUser(email);
        if (resetPasswordSuccess) {
            observableManager.updateMessage("User " + email + " has reseted his password");
        } else {
            observableManager.updateMessage("User " + email + " has failed to reset his password");
        }
        sendResult(resetPasswordSuccess);
    }

    private void checkAndCreateFolder() {
        File dir = new File(email);
        if (!dir.exists()) {
            dir.mkdir();
            observableManager.updateMessage("Client " + email + " has created his rootfolder");
        }
    }

    private void sendResult(boolean result) {
        try {
            socketOut.writeBoolean(result);
        } catch (IOException ex) {
            observableManager.updateMessage("Connection with client failed");
        }
    }

    //TODO
    public boolean compareFolders() {
        return false;
    }

    /*
     ** CLIENT ACTIONS
     */
    public void acceptFile() {
        try {
            String relatiefPath = socketIn.readUTF(); // Receive file name 
            int fileSize = socketIn.readInt(); // Receive file size

            File f = new File(email, relatiefPath);
            File d = new File(f.getParent());
            d.mkdirs();
            DataOutputStream fileOut = new DataOutputStream(new FileOutputStream(f));

            int n, totalSize = 0;
            byte[] buffer = new byte[BUFFERSIZE];
            while (!(totalSize == fileSize)) {
                //System.gc();
                n = BUFFERSIZE;
                totalSize += n;
                if (totalSize > fileSize) {
                    n -= totalSize - fileSize;
                    totalSize = fileSize;
                }
                buffer = new byte[n];
                socketIn.readFully(buffer);
                fileOut.write(buffer);
                //fileOut.flush();
            }
            fileOut.flush();
            fileOut.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public void deleteFile(String relatiefPath) throws IOException {
        System.out.println("DELETING: " + relatiefPath);
        File file = new File(email, relatiefPath);
        observableManager.updateMessage(email + " deletes file: " + file.getName());

        if (file.exists()) {
            if (!file.canWrite()) {
                throw new IllegalArgumentException("Delete: write protected: " + file.getName());
            }
            
            Utils.deleteFileAndEmptyParents(file.toPath(), email);
        }
    }

    public void deleteFile() {
        try {
            deleteFile(socketIn.readUTF()); // Receive file name
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /*
     ** SERVER ACTIONS
     */
    public void sendFile(String relatiefPath) {
        try {
            File file = new File(email, relatiefPath);
            int fileSize = (int) file.length();

            observableManager.updateMessage("Sending " + file.getName() + " to " + email);

            socketOut.writeUTF("acceptFile");
            socketOut.writeUTF(dir.toURI().relativize(file.toURI()).getPath()); // Send file name
            socketOut.writeInt(fileSize); // Send file size

            int n;
            byte[] buffer = new byte[BUFFERSIZE];
            byte[] bufferS;
            DataInputStream fileIn = new DataInputStream(new FileInputStream(file));
            while ((n = fileIn.read(buffer)) > 0) {
                bufferS = new byte[n];
                System.arraycopy(buffer, 0, bufferS, 0, n);
                socketOut.write(bufferS, 0, n);
                //socketOut.flush();
            }
            fileIn.close();
            socketOut.flush();
        } catch (IOException ex) {
            Logger.getLogger(ClientConnection.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void deleteClientFile(String relatiefPath) {
        try {
            socketOut.writeUTF("deleteFile");
            File file = new File(email, email + File.pathSeparator + relatiefPath);
            socketOut.writeUTF(dir.toURI().relativize(file.toURI()).getPath());
        } catch (IOException ex) {
            Logger.getLogger(ClientConnection.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /*
     ** SYNCHRONIZATION
     */
    public void synchronizeFiles() {
        observableManager.updateMessage(email + " synchronizing files...");

        dir = new File(this.email);
        List<File> fileList = Utils.getFileList(dir);

        for (File file : fileList) {
            try {
                System.out.println("writing");
                socketOut.writeUTF("file");
                System.out.println("wrote");
                socketOut.writeUTF(dir.toURI().relativize(file.toURI()).getPath());
                socketOut.writeLong(Files.getLastModifiedTime(file.toPath()).toMillis());
                socketOut.write(Utils.calculateFileHash(file));

                String fileStatus = socketIn.readUTF();
                System.out.println(dir.toURI().relativize(file.toURI()).getPath() + " : " + fileStatus);

                if (fileStatus.equals("changed_client")) {
                    deleteFile(dir.toURI().relativize(file.toURI()).getPath());
                    if (socketIn.readUTF().equals("acceptFile")) {
                        acceptFile();
                    }
                } else if (fileStatus.equals("changed_server")) {
                    sendFile(dir.toURI().relativize(file.toURI()).getPath());
                } else if (fileStatus.equals("notfound")) {
                    sendFile(dir.toURI().relativize(file.toURI()).getPath());
                    System.out.println("uploaded");
                }
            } catch (IOException e) {
                e.printStackTrace();
                observableManager.updateMessage("Error synchronizing file: " + file.getName());
            }
        }

        try {
            socketOut.writeUTF("end");

            while (socketIn.readUTF().equals("acceptFile")) {
                acceptFile();
            }
        } catch (IOException e) {
            observableManager.updateMessage("Error synchronizing client file.");
        }

        System.out.println("END!!!!!!!!!!!!");
    }
}